OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 265 LINES
 
[ HISTORY ]  [ UP ]
 

---
// Unified Characters leaderboard page. Counterpart to /players/ - same
// 4 combinations (season1|season2|all-time ? time|runs) but per-character
// (not account-grouped). Class filter is only available here since accounts
// span multiple classes.
import PageLayout from "../../../../layouts/PageLayout.astro";
import LeaderboardTable from "../../../../components/Leaderboard/LeaderboardTable/
LeaderboardTable.astro";
import Pagination from "../../../../components/Leaderboard/Pagination/Pagination.a
stro";
import LeaderboardScopeFilter from "../../../../components/Leaderboard/Leaderboard
ScopeFilter/LeaderboardScopeFilter.astro";
import ClassFilterSelect from "../../../../components/PlayerProfile/ClassFilterSel
ect/ClassFilterSelect.astro";
import LeaderboardTypeNav from "../../../../components/Leaderboard/LeaderboardType
Nav/LeaderboardTypeNav.astro";
import LeaderboardHeader from "../../../../components/Leaderboard/LeaderboardHeade
r/LeaderboardHeader.astro";
import PlayerSearch from "../../../../components/PlayerSearch/PlayerSearch.astro";
import { fetchUnifiedLeaderboard } from "../../../../lib/api";
import { getEffectiveRealmSlug } from "../../../../lib/realms";
import { CLASSES } from "../../../../lib/wow-constants";

export const prerender = false;

const seasonParam = Astro.params.season || "season3";
const seasonMatch = seasonParam.match(/^season(\d+)$/);
const isAllTime = seasonParam === "all-time";
const currentSeason: number | "all-time" = isAllTime
  ? "all-time"
  : seasonMatch
    ? parseInt(seasonMatch[1], 10)
    : 3;

const scope = Astro.params.scope || "";
let scopeParts = scope.split("/").filter(Boolean);

// Sort lives at the front of the scope catchall as `by-runs` (default
// `by-time` is omitted).
let sortParam: "time" | "runs" = "time";
if (scopeParts[0] === "by-runs") {
  sortParam = "runs";
  scopeParts = scopeParts.slice(1);
}

let regionParam = "global";
let realmParam = "";
let classParam = "all";
let apiScope: "global" | "regional" | "realm" | "class" = "global";

if (scopeParts.length >= 1) {
  regionParam = scopeParts[0];
  if (regionParam === "global") {
    apiScope = "global";
    if (scopeParts.length >= 2) {
      classParam = scopeParts[1];
      apiScope = "class";
    }
  } else {
    if (scopeParts.length === 1) {
      apiScope = "regional";
    } else if (scopeParts.length === 2) {
      const secondPart = scopeParts[1];
      if (CLASSES.includes(secondPart as any)) {
        apiScope = "class";
        classParam = secondPart;
      } else {
        apiScope = "realm";
        realmParam = secondPart;
      }
    } else if (scopeParts.length >= 3) {
      apiScope = "class";
      realmParam = scopeParts[1];
      classParam = scopeParts[2];
    }
  }
}

// Connected-realm rollup: child realm slugs don't have their own JSON.
// 301 to the parent so URL matches the data served. Class scope tail is
// preserved.
if (apiScope === "realm" || apiScope === "class") {
  if (realmParam) {
    const parent = getEffectiveRealmSlug(regionParam, realmParam);
    if (parent && parent !== realmParam) {
      const newSegs = [regionParam, parent];
      if (classParam !== "all") newSegs.push(classParam);
      const newPath = `/challenge-mode/${seasonParam}/characters/${newSegs.join("/
")}`;
      const qs = Astro.url.searchParams.toString();
      return Astro.redirect(qs ? `${newPath}?${qs}` : newPath, 301);
    }
  }
}

const pageParam = Astro.url.searchParams.get("page");
const currentPage = pageParam ? parseInt(pageParam, 10) : 1;

const seasonForNav: number = isAllTime ? 2 : (currentSeason as number);

let scopeLabel: string;
if (regionParam === "global") {
  scopeLabel = "Global";
} else if (realmParam && realmParam !== "all") {
  scopeLabel = `${regionParam.toUpperCase()} - ${realmParam}`;
} else {
  scopeLabel = `${regionParam.toUpperCase()} - All Realms`;
}
const seasonLabel = isAllTime ? "All Time" : `Season ${currentSeason}`;

let data;
try {
  data = await fetchUnifiedLeaderboard(
    "character",
    sortParam,
    currentSeason,
    apiScope,
    {
      region: regionParam,
      realmSlug: realmParam || undefined,
      classKey: classParam !== "all" ? classParam : undefined,
      page: currentPage,
    },
    Astro.url.origin,
  );
} catch (error) {
  console.error("[Characters page] Error:", error);
  throw error;
}

const pageTitle = `Character Rankings - ${scopeLabel} - ${seasonLabel}`;
const dungeonName =
  sortParam === "runs"
    ? "Character Rankings - Total Runs"
    : "Character Rankings";

// Sort lives in the path (/by-runs segment). Toggling rebuilds the path.
const characterScopeTail = scopeParts.length ? scopeParts.join("/") : "global";
const otherParams = new URLSearchParams(Astro.url.searchParams);
otherParams.delete("page");
otherParams.delete("sort");
const otherQS = otherParams.toString();
const qsSuffix = otherQS ? `?${otherQS}` : "";

function buildPath(view: "players" | "characters", sort: "time" | "runs") {
  const sortSeg = sort === "runs" ? "/by-runs" : "";
  if (view === "players") {
    // Player view doesn't support class scope - strip the class slug if it's
    // dangling at the end.
    const segs = scopeParts.slice();
    if (classParam !== "all" && segs[segs.length - 1] === classParam) {
      segs.pop();
    }
    const tail = segs.length ? segs.join("/") : "global";
    return `/challenge-mode/${seasonParam}/players${sortSeg}/${tail}`;
  }
  return `/challenge-mode/${seasonParam}/characters${sortSeg}/${characterScopeTail
}`;
}
const sortByTimeUrl = buildPath("characters", "time") + qsSuffix;
const sortByRunsUrl = buildPath("characters", "runs") + qsSuffix;
const viewPlayerUrl = buildPath("players", sortParam) + qsSuffix;
const viewCharacterUrl = buildPath("characters", sortParam) + qsSuffix;

const tableType = sortParam === "runs" ? "total-runs" : "player";
---

<PageLayout title={pageTitle}>
  <main class="leaderboard-page">
    <LeaderboardHeader
      season={seasonForNav}
      dungeonName={dungeonName}
      scopeLabel={`${seasonLabel} - ${scopeLabel}`}
    />
    <PlayerSearch />
    <LeaderboardTypeNav
      currentTab="player"
      currentRegion={regionParam}
      currentRealm={realmParam}
      currentSeason={seasonForNav}
      currentClass={classParam}
    />

    <div class="cm-filters">
      <div class="filter-group">
        <label for="view-filter">View</label>
        <select id="view-filter">
          <option value="player" data-url={viewPlayerUrl}>Player</option>
          <option value="character" selected data-url={viewCharacterUrl}>
            Character
          </option>
        </select>
      </div>
      <div class="filter-group">
        <label for="sort-filter">Sort</label>
        <select id="sort-filter">
          <option
            value="time"
            selected={sortParam === "time"}
            data-url={sortByTimeUrl}
          >
            Time
          </option>
          <option
            value="runs"
            selected={sortParam === "runs"}
            data-url={sortByRunsUrl}
          >
            Total Runs
          </option>
        </select>
      </div>
      <LeaderboardScopeFilter
        currentRegion={regionParam}
        currentRealm={realmParam}
        currentSeason={isAllTime ? 0 : (currentSeason as number)}
        leaderboardType="character"
        currentClass={classParam}
        currentSort={sortParam}
      />
      <ClassFilterSelect currentClass={classParam} />
    </div>

    <div id="leaderboard-content">
      <LeaderboardTable
        type={tableType}
        players={data.leaderboard}
        currentPage={data.pagination.currentPage}
        pageSize={data.pagination.pageSize}
        region={regionParam}
        realm={realmParam}
      />
    </div>

    <Pagination
      currentPage={data.pagination.currentPage}
      totalPages={data.pagination.totalPages}
      hasNextPage={data.pagination.hasNextPage}
      hasPrevPage={data.pagination.hasPrevPage}
      totalPlayers={data.pagination.totalPlayers ??
        data.pagination.totalRuns ??
        0}
      baseUrl={Astro.url.pathname}
    />
  </main>
</PageLayout>

<script>
  for (const id of ["view-filter", "sort-filter"]) {
    const el = document.getElementById(id) as HTMLSelectElement | null;
    if (!el) continue;
    el.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectElement;
      const url = target.options[target.selectedIndex].dataset.url;
      if (url) window.location.href = url;
    });
  }
</script>

<style>
  .leaderboard-page {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  @media (max-width: 768px) {
    .leaderboard-page {
      padding: 15px;
    }
  }
</style>

---
// Unified Characters leaderboard page. Coun
terpart to /players/ - same
// 4 combinations (season1|season2|all-time 
? time|runs) but per-character
// (not account-grouped). Class filter is on
ly available here since accounts
// span multiple classes.
import PageLayout from "../../../../layouts/
PageLayout.astro";
import LeaderboardTable from "../../../../co
mponents/Leaderboard/LeaderboardTable/Leader
boardTable.astro";
import Pagination from "../../../../componen
ts/Leaderboard/Pagination/Pagination.astro";
import LeaderboardScopeFilter from "../../..
/../components/Leaderboard/LeaderboardScopeF
ilter/LeaderboardScopeFilter.astro";
import ClassFilterSelect from "../../../../c
omponents/PlayerProfile/ClassFilterSelect/Cl
assFilterSelect.astro";
import LeaderboardTypeNav from "../../../../
components/Leaderboard/LeaderboardTypeNav/Le
aderboardTypeNav.astro";
import LeaderboardHeader from "../../../../c
omponents/Leaderboard/LeaderboardHeader/Lead
erboardHeader.astro";
import PlayerSearch from "../../../../compon
ents/PlayerSearch/PlayerSearch.astro";
import { fetchUnifiedLeaderboard } from "../
../../../lib/api";
import { getEffectiveRealmSlug } from "../..
/../../lib/realms";
import { CLASSES } from "../../../../lib/wow
-constants";

export const prerender = false;

const seasonParam = Astro.params.season || "
season3";
const seasonMatch = seasonParam.match(/^seas
on(\d+)$/);
const isAllTime = seasonParam === "all-time"
;
const currentSeason: number | "all-time" = i
sAllTime
  ? "all-time"
  : seasonMatch
    ? parseInt(seasonMatch[1], 10)
    : 3;

const scope = Astro.params.scope || "";
let scopeParts = scope.split("/").filter(Boo
lean);

// Sort lives at the front of the scope catc
hall as `by-runs` (default
// `by-time` is omitted).
let sortParam: "time" | "runs" = "time";
if (scopeParts[0] === "by-runs") {
  sortParam = "runs";
  scopeParts = scopeParts.slice(1);
}

let regionParam = "global";
let realmParam = "";
let classParam = "all";
let apiScope: "global" | "regional" | "realm
" | "class" = "global";

if (scopeParts.length >= 1) {
  regionParam = scopeParts[0];
  if (regionParam === "global") {
    apiScope = "global";
    if (scopeParts.length >= 2) {
      classParam = scopeParts[1];
      apiScope = "class";
    }
  } else {
    if (scopeParts.length === 1) {
      apiScope = "regional";
    } else if (scopeParts.length === 2) {
      const secondPart = scopeParts[1];
      if (CLASSES.includes(secondPart as any
)) {
        apiScope = "class";
        classParam = secondPart;
      } else {
        apiScope = "realm";
        realmParam = secondPart;
      }
    } else if (scopeParts.length >= 3) {
      apiScope = "class";
      realmParam = scopeParts[1];
      classParam = scopeParts[2];
    }
  }
}

// Connected-realm rollup: child realm slugs
 don't have their own JSON.
// 301 to the parent so URL matches the data
 served. Class scope tail is
// preserved.
if (apiScope === "realm" || apiScope === "cl
ass") {
  if (realmParam) {
    const parent = getEffectiveRealmSlug(reg
ionParam, realmParam);
    if (parent && parent !== realmParam) {
      const newSegs = [regionParam, parent];
      if (classParam !== "all") newSegs.push
(classParam);
      const newPath = `/challenge-mode/${sea
sonParam}/characters/${newSegs.join("/")}`;
      const qs = Astro.url.searchParams.toSt
ring();
      return Astro.redirect(qs ? `${newPath}
?${qs}` : newPath, 301);
    }
  }
}

const pageParam = Astro.url.searchParams.get
("page");
const currentPage = pageParam ? parseInt(pag
eParam, 10) : 1;

const seasonForNav: number = isAllTime ? 2 :
 (currentSeason as number);

let scopeLabel: string;
if (regionParam === "global") {
  scopeLabel = "Global";
} else if (realmParam && realmParam !== "all
") {
  scopeLabel = `${regionParam.toUpperCase()}
 - ${realmParam}`;
} else {
  scopeLabel = `${regionParam.toUpperCase()}
 - All Realms`;
}
const seasonLabel = isAllTime ? "All Time" :
 `Season ${currentSeason}`;

let data;
try {
  data = await fetchUnifiedLeaderboard(
    "character",
    sortParam,
    currentSeason,
    apiScope,
    {
      region: regionParam,
      realmSlug: realmParam || undefined,
      classKey: classParam !== "all" ? class
Param : undefined,
      page: currentPage,
    },
    Astro.url.origin,
  );
} catch (error) {
  console.error("[Characters page] Error:", 
error);
  throw error;
}

const pageTitle = `Character Rankings - ${sc
opeLabel} - ${seasonLabel}`;
const dungeonName =
  sortParam === "runs"
    ? "Character Rankings - Total Runs"
    : "Character Rankings";

// Sort lives in the path (/by-runs segment)
. Toggling rebuilds the path.
const characterScopeTail = scopeParts.length
 ? scopeParts.join("/") : "global";
const otherParams = new URLSearchParams(Astr
o.url.searchParams);
otherParams.delete("page");
otherParams.delete("sort");
const otherQS = otherParams.toString();
const qsSuffix = otherQS ? `?${otherQS}` : "
";

function buildPath(view: "players" | "charac
ters", sort: "time" | "runs") {
  const sortSeg = sort === "runs" ? "/by-run
s" : "";
  if (view === "players") {
    // Player view doesn't support class sco
pe - strip the class slug if it's
    // dangling at the end.
    const segs = scopeParts.slice();
    if (classParam !== "all" && segs[segs.le
ngth - 1] === classParam) {
      segs.pop();
    }
    const tail = segs.length ? segs.join("/"
) : "global";
    return `/challenge-mode/${seasonParam}/p
layers${sortSeg}/${tail}`;
  }
  return `/challenge-mode/${seasonParam}/cha
racters${sortSeg}/${characterScopeTail}`;
}
const sortByTimeUrl = buildPath("characters"
, "time") + qsSuffix;
const sortByRunsUrl = buildPath("characters"
, "runs") + qsSuffix;
const viewPlayerUrl = buildPath("players", s
ortParam) + qsSuffix;
const viewCharacterUrl = buildPath("characte
rs", sortParam) + qsSuffix;

const tableType = sortParam === "runs" ? "to
tal-runs" : "player";
---

<PageLayout title={pageTitle}>
  <main class="leaderboard-page">
    <LeaderboardHeader
      season={seasonForNav}
      dungeonName={dungeonName}
      scopeLabel={`${seasonLabel} - ${scopeL
abel}`}
    />
    <PlayerSearch />
    <LeaderboardTypeNav
      currentTab="player"
      currentRegion={regionParam}
      currentRealm={realmParam}
      currentSeason={seasonForNav}
      currentClass={classParam}
    />

    <div class="cm-filters">
      <div class="filter-group">
        <label for="view-filter">View</label
>
        <select id="view-filter">
          <option value="player" data-url={v
iewPlayerUrl}>Player</option>
          <option value="character" selected
 data-url={viewCharacterUrl}>
            Character
          </option>
        </select>
      </div>
      <div class="filter-group">
        <label for="sort-filter">Sort</label
>
        <select id="sort-filter">
          <option
            value="time"
            selected={sortParam === "time"}
            data-url={sortByTimeUrl}
          >
            Time
          </option>
          <option
            value="runs"
            selected={sortParam === "runs"}
            data-url={sortByRunsUrl}
          >
            Total Runs
          </option>
        </select>
      </div>
      <LeaderboardScopeFilter
        currentRegion={regionParam}
        currentRealm={realmParam}
        currentSeason={isAllTime ? 0 : (curr
entSeason as number)}
        leaderboardType="character"
        currentClass={classParam}
        currentSort={sortParam}
      />
      <ClassFilterSelect currentClass={class
Param} />
    </div>

    <div id="leaderboard-content">
      <LeaderboardTable
        type={tableType}
        players={data.leaderboard}
        currentPage={data.pagination.current
Page}
        pageSize={data.pagination.pageSize}
        region={regionParam}
        realm={realmParam}
      />
    </div>

    <Pagination
      currentPage={data.pagination.currentPa
ge}
      totalPages={data.pagination.totalPages
}
      hasNextPage={data.pagination.hasNextPa
ge}
      hasPrevPage={data.pagination.hasPrevPa
ge}
      totalPlayers={data.pagination.totalPla
yers ??
        data.pagination.totalRuns ??
        0}
      baseUrl={Astro.url.pathname}
    />
  </main>
</PageLayout>

<script>
  for (const id of ["view-filter", "sort-fil
ter"]) {
    const el = document.getElementById(id) a
s HTMLSelectElement | null;
    if (!el) continue;
    el.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectE
lement;
      const url = target.options[target.sele
ctedIndex].dataset.url;
      if (url) window.location.href = url;
    });
  }
</script>

<style>
  .leaderboard-page {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  @media (max-width: 768px) {
    .leaderboard-page {
      padding: 15px;
    }
  }
</style>
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET